import Cesium from './Cesium'

/**
 * @example
 * // Create a framebuffer with color and depth texture attachments.
 * const width = context.canvas.clientWidth;
 * const height = context.canvas.clientHeight;
 * const framebuffer = new Framebuffer({
 *   context : context,
 *   colorTextures : [new Texture({
 *     context : context,
 *     width : width,
 *     height : height,
 *     pixelFormat : PixelFormat.RGBA
 *   })],
 *   depthTexture : new Texture({
 *     context : context,
 *     width : width,
 *     height : height,
 *     pixelFormat : PixelFormat.DEPTH_COMPONENT,
 *     pixelDatatype : PixelDatatype.UNSIGNED_SHORT
 *   })
 * });
 */

function attachTexture(framebuffer, attachment, texture, layer = 0) {
  const gl = framebuffer._gl
  if (texture.depth > 1) {
    gl.framebufferTextureLayer(gl.FRAMEBUFFER, attachment, texture._texture, 0, layer)
  } else {
    gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, texture._target, texture._texture, 0)
  }
}

function attachRenderbuffer(framebuffer, attachment, renderbuffer) {
  const gl = framebuffer._gl
  gl.framebufferRenderbuffer(
    gl.FRAMEBUFFER,
    attachment,
    gl.RENDERBUFFER,
    renderbuffer._getRenderbuffer(),
  )
}

// class WeFramebuffer extends Cesium.Framebuffer {
class WeFramebuffer {
  _activeColorAttachments
  _gl
  _framebuffer
  _colorTextures
  _colorRenderbuffers
  _depthTexture
  _depthRenderbuffer
  _stencilRenderbuffer
  _depthStencilTexture
  _depthStencilRenderbuffer
  destroyAttachments
  currentLayer
  lastLayer

  constructor(options) {
    options = options ?? Cesium.Frozen.EMPTY_OBJECT

    const context = options.context
    //>>includeStart('debug', pragmas.debug);
    Cesium.Check.defined('options.context', context)
    //>>includeEnd('debug');

    const gl = context._gl
    const maximumColorAttachments = Cesium.ContextLimits.maximumColorAttachments

    this._gl = gl
    this._framebuffer = gl.createFramebuffer()

    this._colorTextures = []
    this._colorRenderbuffers = []
    this._activeColorAttachments = []

    this._depthTexture = undefined
    this._depthRenderbuffer = undefined
    this._stencilRenderbuffer = undefined
    this._depthStencilTexture = undefined
    this._depthStencilRenderbuffer = undefined

    /**
     * When true, the framebuffer owns its attachments so they will be destroyed when
     * {@link Framebuffer#destroy} is called or when a new attachment is assigned
     * to an attachment point.
     *
     * @type {boolean}
     * @default true
     *
     * @see Framebuffer#destroy
     */
    this.destroyAttachments = options.destroyAttachments ?? true

    // Throw if a texture and renderbuffer are attached to the same point.  This won't
    // cause a WebGL error (because only one will be attached), but is likely a developer error.

    //>>includeStart('debug', pragmas.debug);
    if (Cesium.defined(options.colorTextures) && Cesium.defined(options.colorRenderbuffers)) {
      throw new Cesium.DeveloperError(
        'Cannot have both color texture and color renderbuffer attachments.',
      )
    }
    if (Cesium.defined(options.depthTexture) && Cesium.defined(options.depthRenderbuffer)) {
      throw new Cesium.DeveloperError(
        'Cannot have both a depth texture and depth renderbuffer attachment.',
      )
    }
    if (
      Cesium.defined(options.depthStencilTexture) &&
      Cesium.defined(options.depthStencilRenderbuffer)
    ) {
      throw new Cesium.DeveloperError(
        'Cannot have both a depth-stencil texture and depth-stencil renderbuffer attachment.',
      )
    }

    // Avoid errors defined in Section 6.5 of the WebGL spec
    const depthAttachment =
      Cesium.defined(options.depthTexture) || Cesium.defined(options.depthRenderbuffer)
    const depthStencilAttachment =
      Cesium.defined(options.depthStencilTexture) ||
      Cesium.defined(options.depthStencilRenderbuffer)
    if (depthAttachment && depthStencilAttachment) {
      throw new Cesium.DeveloperError('Cannot have both a depth and depth-stencil attachment.')
    }
    if (Cesium.defined(options.stencilRenderbuffer) && depthStencilAttachment) {
      throw new Cesium.DeveloperError('Cannot have both a stencil and depth-stencil attachment.')
    }
    if (depthAttachment && Cesium.defined(options.stencilRenderbuffer)) {
      throw new Cesium.DeveloperError('Cannot have both a depth and stencil attachment.')
    }
    //>>includeEnd('debug');

    this._bind()

    if (Cesium.defined(options.colorTextures)) {
      const textures = options.colorTextures
      const length =
        (this._colorTextures.length =
          this._activeColorAttachments.length =
          textures.length)

      //>>includeStart('debug', pragmas.debug);
      if (length > maximumColorAttachments) {
        throw new Cesium.DeveloperError(
          'The number of color attachments exceeds the number supported.',
        )
      }
      //>>includeEnd('debug');

      for (let i = 0; i < length; ++i) {
        const texture = textures[i]

        //>>includeStart('debug', pragmas.debug);
        if (!Cesium.PixelFormat.isColorFormat(texture.pixelFormat)) {
          throw new Cesium.DeveloperError('The color-texture pixel-format must be a color format.')
        }
        if (texture.pixelDatatype === Cesium.PixelDatatype.FLOAT && !context.colorBufferFloat) {
          throw new Cesium.DeveloperError(
            'The color texture pixel datatype is FLOAT and the WebGL implementation does not support the EXT_color_buffer_float or WEBGL_color_buffer_float extensions. See Context.colorBufferFloat.',
          )
        }
        if (
          texture.pixelDatatype === Cesium.PixelDatatype.HALF_FLOAT &&
          !context.colorBufferHalfFloat
        ) {
          throw new Cesium.DeveloperError(
            'The color texture pixel datatype is HALF_FLOAT and the WebGL implementation does not support the EXT_color_buffer_half_float extension. See Context.colorBufferHalfFloat.',
          )
        }
        //>>includeEnd('debug');

        const attachmentEnum = this._gl.COLOR_ATTACHMENT0 + i
        attachTexture(this, attachmentEnum, texture)
        this._activeColorAttachments[i] = attachmentEnum
        this._colorTextures[i] = texture
      }
    }

    if (Cesium.defined(options.colorRenderbuffers)) {
      const renderbuffers = options.colorRenderbuffers
      const length =
        (this._colorRenderbuffers.length =
          this._activeColorAttachments.length =
          renderbuffers.length)

      //>>includeStart('debug', pragmas.debug);
      if (length > maximumColorAttachments) {
        throw new Cesium.DeveloperError(
          'The number of color attachments exceeds the number supported.',
        )
      }
      //>>includeEnd('debug');

      for (let i = 0; i < length; ++i) {
        const renderbuffer = renderbuffers[i]
        const attachmentEnum = this._gl.COLOR_ATTACHMENT0 + i
        attachRenderbuffer(this, attachmentEnum, renderbuffer)
        this._activeColorAttachments[i] = attachmentEnum
        this._colorRenderbuffers[i] = renderbuffer
      }
    }

    if (Cesium.defined(options.depthTexture)) {
      const texture = options.depthTexture

      //>>includeStart('debug', pragmas.debug);
      if (texture.pixelFormat !== Cesium.PixelFormat.DEPTH_COMPONENT) {
        throw new Cesium.DeveloperError('The depth-texture pixel-format must be DEPTH_COMPONENT.')
      }
      //>>includeEnd('debug');

      attachTexture(this, this._gl.DEPTH_ATTACHMENT, texture)
      this._depthTexture = texture
    }

    if (Cesium.defined(options.depthRenderbuffer)) {
      const renderbuffer = options.depthRenderbuffer
      attachRenderbuffer(this, this._gl.DEPTH_ATTACHMENT, renderbuffer)
      this._depthRenderbuffer = renderbuffer
    }

    if (Cesium.defined(options.stencilRenderbuffer)) {
      const renderbuffer = options.stencilRenderbuffer
      attachRenderbuffer(this, this._gl.STENCIL_ATTACHMENT, renderbuffer)
      this._stencilRenderbuffer = renderbuffer
    }

    if (Cesium.defined(options.depthStencilTexture)) {
      const texture = options.depthStencilTexture

      //>>includeStart('debug', pragmas.debug);
      if (texture.pixelFormat !== Cesium.PixelFormat.DEPTH_STENCIL) {
        throw new Cesium.DeveloperError('The depth-stencil pixel-format must be DEPTH_STENCIL.')
      }
      //>>includeEnd('debug');

      attachTexture(this, this._gl.DEPTH_STENCIL_ATTACHMENT, texture)
      this._depthStencilTexture = texture
    }

    if (Cesium.defined(options.depthStencilRenderbuffer)) {
      const renderbuffer = options.depthStencilRenderbuffer
      attachRenderbuffer(this, this._gl.DEPTH_STENCIL_ATTACHMENT, renderbuffer)
      this._depthStencilRenderbuffer = renderbuffer
    }

    this._unBind()
  }

  /**
   * The status of the framebuffer. If the status is not WebGLConstants.FRAMEBUFFER_COMPLETE,
   * a {@link DeveloperError} will be thrown when attempting to render to the framebuffer.
   * @memberof Framebuffer.prototype
   * @type {number}
   */
  get status() {
    this._bind()
    const status = this._gl.checkFramebufferStatus(this._gl.FRAMEBUFFER)
    this._unBind()
    return status
  }

  get numberOfColorAttachments() {
    return this._activeColorAttachments.length
  }

  get depthTexture() {
    return this._depthTexture
  }

  get depthRenderbuffer() {
    return this._depthRenderbuffer
  }

  get stencilRenderbuffer() {
    return this._stencilRenderbuffer
  }

  get depthStencilTexture() {
    return this._depthStencilTexture
  }

  get depthStencilRenderbuffer() {
    return this._depthStencilRenderbuffer
  }

  /**
   * True if the framebuffer has a depth attachment.  Depth attachments include
   * depth and depth-stencil textures, and depth and depth-stencil renderbuffers.  When
   * rendering to a framebuffer, a depth attachment is required for the depth test to have effect.
   * @memberof Framebuffer.prototype
   * @type {boolean}
   */
  get hasDepthAttachment() {
    return !!(
      this.depthTexture ||
      this.depthRenderbuffer ||
      this.depthStencilTexture ||
      this.depthStencilRenderbuffer
    )
  }

  _bind() {
    const gl = this._gl
    gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebuffer)
    if (this.lastLayer !== this.currentLayer) {
      this.lastLayer = this.currentLayer
      const size = this._colorTextures.length
      for (let i = 0; i < size; i++) {
        const texture = this._colorTextures[i]
        if (texture.depth > 0 && this.currentLayer < texture.depth) {
          const attachmentEnum = this._gl.COLOR_ATTACHMENT0 + i
          attachTexture(this, attachmentEnum, texture, this.currentLayer)
        }
      }
    }
  }

  _unBind() {
    const gl = this._gl
    gl.bindFramebuffer(gl.FRAMEBUFFER, null)
  }

  bindDraw() {
    const gl = this._gl
    gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._framebuffer)
  }

  bindRead() {
    const gl = this._gl
    gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this._framebuffer)
  }

  _getActiveColorAttachments() {
    return this._activeColorAttachments
  }

  getColorTexture(index) {
    //>>includeStart('debug', pragmas.debug);
    if (!Cesium.defined(index) || index < 0 || index >= this._colorTextures.length) {
      throw new Cesium.DeveloperError(
        'index is required, must be greater than or equal to zero and must be less than the number of color attachments.',
      )
    }
    //>>includeEnd('debug');

    return this._colorTextures[index]
  }

  getColorRenderbuffer(index) {
    //>>includeStart('debug', pragmas.debug);
    if (!Cesium.defined(index) || index < 0 || index >= this._colorRenderbuffers.length) {
      throw new Cesium.DeveloperError(
        'index is required, must be greater than or equal to zero and must be less than the number of color attachments.',
      )
    }
    //>>includeEnd('debug');

    return this._colorRenderbuffers[index]
  }

  isDestroyed() {
    return false
  }

  destroy() {
    if (this.destroyAttachments) {
      // If the color texture is a cube map face, it is owned by the cube map, and will not be destroyed.
      const textures = this._colorTextures
      for (let i = 0; i < textures.length; ++i) {
        const texture = textures[i]
        if (Cesium.defined(texture)) {
          texture.destroy()
        }
      }

      const renderbuffers = this._colorRenderbuffers
      for (let i = 0; i < renderbuffers.length; ++i) {
        const renderbuffer = renderbuffers[i]
        if (Cesium.defined(renderbuffer)) {
          renderbuffer.destroy()
        }
      }

      this._depthTexture = this._depthTexture && this._depthTexture.destroy()
      this._depthRenderbuffer = this._depthRenderbuffer && this._depthRenderbuffer.destroy()
      this._stencilRenderbuffer = this._stencilRenderbuffer && this._stencilRenderbuffer.destroy()
      this._depthStencilTexture = this._depthStencilTexture && this._depthStencilTexture.destroy()
      this._depthStencilRenderbuffer =
        this._depthStencilRenderbuffer && this._depthStencilRenderbuffer.destroy()
    }

    this._gl.deleteFramebuffer(this._framebuffer)
    return Cesium.destroyObject(this)
  }
}

export default WeFramebuffer
